﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;

namespace CodeContrib
{
	public class Calculator
	{
		// Flag to determine whether the decimal separator for the number has been read.
		private bool _expressionStart;
		private bool _hasDecimalSeparator;
		private bool _hasPercentSymbol;
		private bool _isNumber;

		// The harvested number string.
		private readonly StringBuilder _numberHarvested;
		private readonly Stack<decimal> _numbers;

		// The operation stack(for the Dijkstra's rating algorithm).
		private readonly Stack<char> _operationStack;

		public Calculator()
		{
			_numberHarvested = new StringBuilder();
			_numbers = new Stack<decimal>();
			_operationStack = new Stack<char>();
		}

		///<summary>
		///	Calculates an expression.
		///	Uses the Dijkstra's Rating Algorithm to represent and calculate
		///	the expression in the Reverse Polish Notation.
		///</summary>
		public decimal Calculate(string expression)
		{
			Guard.NotNullOrEmpty(expression, "expression");

			expression = expression.RemoveWhitespace();

			Reset();

			var decimalSeparator = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
			var negativeSign = Thread.CurrentThread.CurrentCulture.NumberFormat.NegativeSign[0];
			var percentSymbol = Thread.CurrentThread.CurrentCulture.NumberFormat.PercentSymbol[0];

			for (var i = 0; i < expression.Length; i++)
			{
				var character = expression[i];
				int rating;

				if (Char.IsWhiteSpace(character))
				{
					SaveNumberHarvested();
				}
				else if ((character == negativeSign && _expressionStart)
				         || Char.IsDigit(character))
				{
					// The char is digit or negative sign, start/continue the number harvesting.
					ReadDigit(character);
				}
				else if (character == decimalSeparator)
				{
					// The char is decimal separator; start/continue the number.
					if (!ReadDecimalSeparator(character))
					{
						throw new ArgumentException("A number can have only one decimal separator.");
					}
				}
				else if (character == percentSymbol)
				{
					// The character is a percent separator; start/continue the number
					if (!ReadPercentSymbol(character))
					{
						throw new ArgumentException("The percent symbol must follow the second operand.");
					}
				}
				else if ((rating = GetOperatorRating(character))
				         != -1)
				{
					// The char is operation, save the number.
					SaveNumberHarvested();

					if (character == ')')
					{
						// The operation is a closing bracket; pop all operations to the stack until the opening bracket.
						char operation;

						do
						{
							operation = _operationStack.Pop();

							if (operation != '(')
							{
								PerformOperation(operation);

								if (!_operationStack.Any())
								{
									// The operation stack doesnt contain a corresponding opening bracket.
									throw new ArgumentException("The expression contains a closing bracket without a corresponding opening bracket.");
								}
							}
						}
						while (operation != '(');
					}
					else if (character == '(')
					{
						// The operation is open bracket; add it to the operations stack.
						_operationStack.Push(character);
					}
					else
					{
						// Pop all operations from the stack to the result until top operation rating is less then the current.
						while (_operationStack.Count > 0
						       && GetOperatorRating(_operationStack.Peek()) >= rating)
						{
							PerformOperation(_operationStack.Pop());
						}

						_operationStack.Push(character);
					}
				}
				else
				{
					// Unknown symbol.
					throw new ArgumentException(string.Format("Unknown symbol {0} in the expression", character));
				}

				// Update the expression start flag.
				_expressionStart = (character == '(');
			}

			// Add the remaining number.
			SaveNumberHarvested();

			// Perform remaining operations.
			while (_operationStack.Any())
			{
				var operation = _operationStack.Pop();

				if (operation == '(')
				{
					// The operation stack doesnt contain a corresponding closing bracket.
					throw new ArgumentException("The expression contains an opening bracket without a corresponding closing bracket");
				}

				PerformOperation(operation);
			}

			// The last number in the stack should contain the result.
			return _numbers.Pop();
		}

		/// <summary>
		/// 	Returns the operation rating for the Dijkstra's stack rating algorithm
		/// 	or -1 if the operation is unknown or is ')'
		/// </summary>
		private static int GetOperatorRating(char op)
		{
			var result = -1;

			switch (op)
			{
				case '(':
					{
						result = 0;

						break;
					}
				case ')':
					{
						result = 1;

						break;
					}
				case '-':
				case '+':
					{
						result = 2;

						break;
					}
				case '/':
				case '*':
					{
						result = 3;

						break;
					}
			}

			return result;
		}

		/// <summary>
		/// 	Performs the operation on the numbers stack.
		/// </summary>
		private void PerformOperation(char operation)
		{
			if (_numbers.Count < 2)
			{
				// The operator doesn't have enough operands.
				throw new ArgumentException(string.Format("Operator {0} doesn't have enough operands", operation));
			}

			// Pop the last two numbers.
			var b = _numbers.Pop();
			var a = _numbers.Pop();
			decimal c = 0;

			if (_hasPercentSymbol)
			{
				_hasPercentSymbol = false;

				b = ((a / 100) * b);
			}

			// Apply the operation.
			switch (operation)
			{
				case '+':
					{
						c = a + b;

						break;
					}
				case '-':
					{
						c = a - b;

						break;
					}
				case '*':
					{
						c = a * b;

						break;
					}
				case '/':
					{
						c = a / b;

						break;
					}
				default:
					{
						// The operation is unknown.
						Debug.Assert(false);

						break;
					}
			}

			// Save the result.
			_numbers.Push(c);
		}

		/// <summary>
		/// 	Reads the decimal separator and makes sure the number
		/// 	has only one instance.
		/// </summary>
		private bool ReadDecimalSeparator(char character)
		{
			if (_hasDecimalSeparator)
			{
				return false;
			}

			_hasDecimalSeparator = true;
			_isNumber = true;
			_numberHarvested.Append(character);

			return true;
		}

		/// <summary>
		/// 	Reads the digit or negative symbol.
		/// </summary>
		private void ReadDigit(char c)
		{
			_isNumber = true;
			_numberHarvested.Append(c);
		}

		/// <summary>
		/// 	Reads the percent symbol and makes sure it follows the second operand.
		/// </summary>
		private bool ReadPercentSymbol(char c)
		{
			if (_operationStack.Count == 0
			    || GetOperatorRating(_operationStack.Peek()) <= 1)
			{
				return false;
			}

			_isNumber = true;
			_hasPercentSymbol = true;

			return true;
		}

		/// <summary>
		/// 	Resets internal harvester fields.
		/// </summary>
		private void Reset()
		{
			_numbers.Clear();
			_expressionStart = true;
			_operationStack.Clear();
			ResetNumberHarvester();
		}

		/// <summary>
		/// 	Resets the number harverster fields.
		/// </summary>
		private void ResetNumberHarvester()
		{
			_hasDecimalSeparator = false;
			_isNumber = false;
			_numberHarvested.Clear();
		}

		/// <summary>
		/// 	Saves the harvested number.
		/// </summary>
		private bool SaveNumberHarvested()
		{
			if (!_isNumber)
			{
				return false;
			}

			_numbers.Push(decimal.Parse(_numberHarvested.ToString()));

			ResetNumberHarvester();

			return true;
		}
	}
}